/**
 * Copyright 2019 IBM All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

'use strict';

const rewire = require('rewire');
const ECDSA_KEY_REWIRE = rewire('../../../lib/impl/ecdsa/key');
const jsrsa = require('jsrsasign');
const KEYUTIL = jsrsa.KEYUTIL;

const chai = require('chai');
chai.should();
const sinon = require('sinon');

describe('ECDSA_KEY', () => {

	describe('constructor', () => {

		it('should throw when no params are given', () => {
			(() => {
				new ECDSA_KEY_REWIRE();
			}).should.throw(/The key parameter is required by this key class implementation, whether this instance is for the public key or private key/);
		});

		it('should throw when invalid param passed', () => {
			(() => {
				new ECDSA_KEY_REWIRE({});
			}).should.throw(/This key implementation only supports keys generated by jsrsasign.KEYUTIL. It must have a "type" property of value "EC"/);
		});

		it('should throw when invalid key type passed', () => {
			(() => {
				new ECDSA_KEY_REWIRE({type: 'RSA'});
			}).should.throw(/This key implementation only supports keys generated by jsrsasign.KEYUTIL. It must have a "type" property of value "EC"/);
		});

		it('should throw when parameter with missing pubKeyHex passed', () => {
			(() => {
				new ECDSA_KEY_REWIRE({type: 'EC', prvKeyHex: 'some key value'});
			}).should.throw(/This key implementation only supports keys generated by jsrsasign.KEYUTIL. It must have a "pubKeyHex" property/);
		});

		it('should set the passed key', () => {
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey._key.should.deep.equal(fakeKey);
		});
	});

	describe('#getSKI', () => {

		it('should generate SKI hash string for 256 curve keys', () => {
			const key = new ECDSA_KEY_REWIRE(KEYUTIL.generateKeypair('EC', 'secp256r1').prvKeyObj);
			key.getSKI().length.should.equal(64);
		});

		it('should generate SKI hash string for 384 curve keys', () => {
			const key = new ECDSA_KEY_REWIRE(KEYUTIL.generateKeypair('EC', 'secp384r1').prvKeyObj);
			key.getSKI().length.should.equal(64);
		});

		it('should generate SKI hash string for 256 curve public keys', () => {
			const key = new ECDSA_KEY_REWIRE(KEYUTIL.generateKeypair('EC', 'secp256r1').pubKeyObj);
			key.getSKI().length.should.equal(64);
		});

		it('should generate SKI hash string for 384 curve public keys', () => {
			const key = new ECDSA_KEY_REWIRE(KEYUTIL.generateKeypair('EC', 'secp384r1').pubKeyObj);
			key.getSKI().length.should.equal(64);
		});
	});

	describe('#isSymmetric', () => {

		const keyPair = KEYUTIL.generateKeypair('EC', 'secp256r1');

		it('should return false', () => {
			const key = new ECDSA_KEY_REWIRE(keyPair.prvKeyObj);
			key.isSymmetric().should.equal(false);
		});
	});

	describe('#isPrivate', () => {
		const keyPair = KEYUTIL.generateKeypair('EC', 'secp256r1');

		it('should return true if prvKeyHex exists', () => {
			const key = new ECDSA_KEY_REWIRE(keyPair.prvKeyObj);
			key._key.prvKeyHex = true;
			key.isPrivate().should.equal(true);
		});

		it('should return false if null prvKeyHex', () => {
			const key = new ECDSA_KEY_REWIRE(keyPair.prvKeyObj);
			key._key.prvKeyHex = null;
			key.isPrivate().should.equal(false);
		});
	});

	describe('#getPublicKey', () => {

		it('should return direct if public', () => {

		});

		it('should generate and return a new key if not public', () => {

		});
	});

	describe('#generateCSR', () => {

		let revert;
		afterEach(() => {
			if (revert) {
				revert();
			}
			revert = null;
		});

		it('should throw when trying to generate if public', () => {
			(() => {
				const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
				const myKey = new ECDSA_KEY_REWIRE(fakeKey);
				myKey._key.should.deep.equal(fakeKey);
				myKey.isPrivate = sinon.stub().returns(false);
				myKey.generateCSR('CN=publickey');
			}).should.throw(/A CSR cannot be generated from a public key/);
		});

		it('should rethrow internal errors', () => {
			(() => {
				const pemStub = sinon.stub().throws(new Error('MY_ERROR'));
				const fakeAnsn1 = {
					csr: {
						CSRUtil: {
							newCSRPEM: pemStub
						}
					},
					x509: {
						X500Name: {
							ldapToOneline: sinon.stub()
						}
					}
				};

				revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
				const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
				const myKey = new ECDSA_KEY_REWIRE(fakeKey);
				myKey._key.should.deep.equal(fakeKey);
				myKey.isPrivate = sinon.stub().returns(true);
				myKey.generateCSR('CN=publickey');
			}).should.throw(/MY_ERROR/);
		});

		it('should call into jsra lib if private', () => {

			const pemStub = sinon.stub().returns('your PEM sir');
			const fakeAnsn1 = {
				csr: {
					CSRUtil: {
						newCSRPEM: pemStub
					}
				},
				x509: {
					X500Name: {
						ldapToOneline: sinon.stub()
					}
				}
			};

			revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey._key.should.deep.equal(fakeKey);
			myKey.isPrivate = sinon.stub().returns(true);
			const csr = myKey.generateCSR('CN=publickey');

			csr.should.equal('your PEM sir');
			sinon.assert.calledOnce(pemStub);
		});

		it('should call into jsrsa lib if extensions are passed', () => {

			const extensions = [{subjectAltName: {array: [{dns: 'host1'}, {dns: 'host2'}]}}];
			const pemStub = sinon.stub().returns('your PEM sir');
			const fakeAnsn1 = {
				csr: {
					CSRUtil: {
						newCSRPEM: pemStub
					}
				},
				x509: {
					X500Name: {
						ldapToOneline: sinon.stub()
					}
				}
			};

			revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey.isPrivate = sinon.stub().returns(true);
			const csr = myKey.generateCSR('CN=publickey', extensions);

			csr.should.equal('your PEM sir');
			sinon.assert.calledOnceWithExactly(pemStub, sinon.match.has('ext', extensions));
		});
	});

	describe('#generateX509Certificate', () => {

		let revert;
		afterEach(() => {
			if (revert) {
				revert();
			}
			revert = null;
		});

		it('should throw when trying to generate if public', () => {
			(() => {
				const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
				const myKey = new ECDSA_KEY_REWIRE(fakeKey);
				myKey._key.should.deep.equal(fakeKey);
				myKey.isPrivate = sinon.stub().returns(false);
				myKey.generateX509Certificate('CN=publickey');
			}).should.throw(/An X509 certificate cannot be generated from a public key/);
		});


		it('should rethrow errors', () => {
			(() => {
				const pemStub = sinon.stub().throws(new Error('FORCED_ERROR'));
				const fakeAnsn1 = {
					csr: {
						CSRUtil: {
							newCSRPEM: pemStub
						}
					},
					x509: {
						X500Name: {
							ldapToOneline: sinon.stub()
						},
						X509Util: {
							newCertPEM: pemStub
						}
					}
				};

				revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
				const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
				const myKey = new ECDSA_KEY_REWIRE(fakeKey);
				myKey._key.should.deep.equal(fakeKey);
				myKey.isPrivate = sinon.stub().returns(true);
				myKey.generateX509Certificate('CN=publickey');
			}).should.throw(/FORCED_ERROR/);
		});

		it('should call into jsra lib if private', () => {

			const pemStub = sinon.stub().returns('your PEM sir');
			const fakeAnsn1 = {
				csr: {
					CSRUtil: {
						newCSRPEM: pemStub
					}
				},
				x509: {
					X500Name: {
						ldapToOneline: sinon.stub()
					},
					X509Util: {
						newCertPEM: pemStub
					}
				}
			};

			revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey._key.should.deep.equal(fakeKey);
			myKey.isPrivate = sinon.stub().returns(true);
			const csr = myKey.generateX509Certificate('CN=publickey');

			csr.should.equal('your PEM sir');
			sinon.assert.calledOnce(pemStub);
		});

		it('should set CN to self if no commonName passed', () => {
			const pemStub = sinon.stub().returns('your PEM sir');
			const fakeAnsn1 = {
				csr: {
					CSRUtil: {
						newCSRPEM: pemStub
					}
				},
				x509: {
					X500Name: {
						ldapToOneline: sinon.stub()
					},
					X509Util: {
						newCertPEM: pemStub
					}
				}
			};

			revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey._key.should.deep.equal(fakeKey);
			myKey.isPrivate = sinon.stub().returns(true);
			const csr = myKey.generateX509Certificate();

			csr.should.equal('your PEM sir');
			sinon.assert.calledOnce(pemStub);
			const call = pemStub.getCall(0).args[0];
			call.issuer.str.should.equal('/CN=self');
			call.subject.str.should.equal('/CN=self');
			call.sigalg.name.should.equal('SHA256withECDSA');
		});

		it('should set CN if passed', () => {
			const pemStub = sinon.stub().returns('your PEM sir');
			const fakeAnsn1 = {
				csr: {
					CSRUtil: {
						newCSRPEM: pemStub
					}
				},
				x509: {
					X500Name: {
						ldapToOneline: sinon.stub()
					},
					X509Util: {
						newCertPEM: pemStub
					}
				}
			};

			revert = ECDSA_KEY_REWIRE.__set__('asn1', fakeAnsn1);
			const fakeKey = {type: 'EC', prvKeyHex: 'privateKey', pubKeyHex: 'publicKey'};
			const myKey = new ECDSA_KEY_REWIRE(fakeKey);
			myKey._key.should.deep.equal(fakeKey);
			myKey.isPrivate = sinon.stub().returns(true);
			const csr = myKey.generateX509Certificate('penguin');

			csr.should.equal('your PEM sir');
			sinon.assert.calledOnce(pemStub);
			const call = pemStub.getCall(0).args[0];
			call.issuer.str.should.equal('/CN=penguin');
			call.subject.str.should.equal('/CN=penguin');
			call.sigalg.name.should.equal('SHA256withECDSA');
		});
	});

	describe('#toBytes', () => {

		const keyPair = KEYUTIL.generateKeypair('EC', 'secp256r1');

		let revert;
		let utilStub;
		let pemStub;
		beforeEach(() => {
			pemStub = sinon.stub();
			utilStub = {
				getPEM: pemStub
			};
			revert = ECDSA_KEY_REWIRE.__set__('KEYUTIL', utilStub);
		});

		afterEach(() => {
			revert();
		});

		it('should call key util with key and base if private', () => {
			const key = new ECDSA_KEY_REWIRE(keyPair.prvKeyObj);
			key.isPrivate = sinon.stub().returns(true);
			key.toBytes();
			const args = pemStub.getCall(0).args;
			args.length.should.equal(2);
			args[0].should.deep.equal(key._key);
			args[1].should.equal('PKCS8PRV');
		});

		it('should call key util with key if not private', () => {
			const key = new ECDSA_KEY_REWIRE(keyPair.prvKeyObj);
			key.isPrivate = sinon.stub().returns(false);
			key.toBytes();
			const args = pemStub.getCall(0).args;
			args.length.should.equal(1);
			args[0].should.deep.equal(key._key);
		});
	});

});
